experienceReducer.ts ➔ setExperience   B
last analyzed

Complexity

Conditions 8

Size

Total Lines 45
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 39
dl 0
loc 45
rs 7.0773
c 0
b 0
f 0
cc 8
1
import { combineReducers } from "redux";
2
import {
3
  ExperienceWork,
4
  ExperienceEducation,
5
  ExperienceCommunity,
6
  ExperienceAward,
7
  ExperiencePersonal,
8
  Experience,
9
  ExperienceSkill,
10
} from "../../models/types";
11
import {
12
  ExperienceAction,
13
  FETCH_EXPERIENCE_BY_APPLICANT_SUCCEEDED,
14
  FETCH_EXPERIENCE_BY_APPLICATION_SUCCEEDED,
15
  CREATE_EXPERIENCE_SUCCEEDED,
16
  FETCH_EXPERIENCE_BY_APPLICANT_STARTED,
17
  FETCH_EXPERIENCE_BY_APPLICANT_FAILED,
18
  FETCH_EXPERIENCE_BY_APPLICATION_STARTED,
19
  FETCH_EXPERIENCE_BY_APPLICATION_FAILED,
20
  UPDATE_EXPERIENCE_STARTED,
21
  UPDATE_EXPERIENCE_SUCCEEDED,
22
  UPDATE_EXPERIENCE_FAILED,
23
  DELETE_EXPERIENCE_STARTED,
24
  DELETE_EXPERIENCE_SUCCEEDED,
25
  DELETE_EXPERIENCE_FAILED,
26
  CREATE_EXPERIENCE_SKILL_SUCCEEDED,
27
  UPDATE_EXPERIENCE_SKILL_SUCCEEDED,
28
  DELETE_EXPERIENCE_SKILL_SUCCEEDED,
29
  UPDATE_EXPERIENCE_SKILL_STARTED,
30
  DELETE_EXPERIENCE_SKILL_STARTED,
31
  UPDATE_EXPERIENCE_SKILL_FAILED,
32
  DELETE_EXPERIENCE_SKILL_FAILED,
33
  BATCH_CREATE_EXPERIENCE_SKILLS_SUCCEEDED,
34
  BATCH_UPDATE_EXPERIENCE_SKILLS_SUCCEEDED,
35
  BATCH_DELETE_EXPERIENCE_SKILLS_SUCCEEDED,
36
} from "./experienceActions";
37
import {
38
  mapToObject,
39
  getId,
40
  uniq,
41
  deleteProperty,
42
  mapObjectValues,
43
  flatten,
44
} from "../../helpers/queries";
45
46
export interface ExperienceSection<T> {
47
  byId: {
48
    [id: number]: T;
49
  };
50
  idsByApplicant: {
51
    [applicantId: number]: number[];
52
  };
53
  idsByApplication: {
54
    [applicationId: number]: number[];
55
  };
56
}
57
58
export interface EntityState {
59
  work: ExperienceSection<ExperienceWork>;
60
  education: ExperienceSection<ExperienceEducation>;
61
  community: ExperienceSection<ExperienceCommunity>;
62
  award: ExperienceSection<ExperienceAward>;
63
  personal: ExperienceSection<ExperiencePersonal>;
64
  experienceSkills: {
65
    byId: { [id: number]: ExperienceSkill };
66
    idsByWork: { [workId: number]: number[] };
67
    idsByEducation: { [educationId: number]: number[] };
68
    idsByCommunity: { [communityId: number]: number[] };
69
    idsByAward: { [awardId: number]: number[] };
70
    idsByPersonal: { [personalId: number]: number[] };
71
  };
72
}
73
74
export interface UiState {
75
  updatingByApplicant: {
76
    [id: number]: boolean;
77
  };
78
  updatingByApplication: {
79
    [id: number]: boolean;
80
  };
81
  updatingByTypeAndId: {
82
    work: {
83
      [id: number]: boolean;
84
    };
85
    education: {
86
      [id: number]: boolean;
87
    };
88
    community: {
89
      [id: number]: boolean;
90
    };
91
    award: {
92
      [id: number]: boolean;
93
    };
94
    personal: {
95
      [id: number]: boolean;
96
    };
97
  };
98
  updatingExperienceSkill: {
99
    [id: number]: boolean;
100
  };
101
}
102
103
export interface ExperienceState {
104
  entities: EntityState;
105
  ui: UiState;
106
}
107
108
export const initEntities = (): EntityState => ({
109
  work: {
110
    byId: {},
111
    idsByApplicant: {},
112
    idsByApplication: {},
113
  },
114
  education: {
115
    byId: {},
116
    idsByApplicant: {},
117
    idsByApplication: {},
118
  },
119
  community: {
120
    byId: {},
121
    idsByApplicant: {},
122
    idsByApplication: {},
123
  },
124
  award: {
125
    byId: {},
126
    idsByApplicant: {},
127
    idsByApplication: {},
128
  },
129
  personal: {
130
    byId: {},
131
    idsByApplicant: {},
132
    idsByApplication: {},
133
  },
134
  experienceSkills: {
135
    byId: {},
136
    idsByWork: {},
137
    idsByEducation: {},
138
    idsByCommunity: {},
139
    idsByAward: {},
140
    idsByPersonal: {},
141
  },
142
});
143
144
export const initUi = (): UiState => ({
145
  updatingByApplicant: {},
146
  updatingByApplication: {},
147
  updatingByTypeAndId: {
148
    work: {},
149
    education: {},
150
    community: {},
151
    award: {},
152
    personal: {},
153
  },
154
  updatingExperienceSkill: {},
155
});
156
157
export const initExperienceState = (): ExperienceState => ({
158
  entities: initEntities(),
159
  ui: initUi(),
160
});
161
162
function isWork(experience: Experience): experience is ExperienceWork {
163
  return experience.type === "experience_work";
164
}
165
function isEducation(
166
  experience: Experience,
167
): experience is ExperienceEducation {
168
  return experience.type === "experience_education";
169
}
170
function isCommunity(
171
  experience: Experience,
172
): experience is ExperienceCommunity {
173
  return experience.type === "experience_community";
174
}
175
function isAward(experience: Experience): experience is ExperienceAward {
176
  return experience.type === "experience_award";
177
}
178
function isPersonal(experience: Experience): experience is ExperiencePersonal {
179
  return experience.type === "experience_personal";
180
}
181
182
type EntityType = "work" | "education" | "community" | "award" | "personal";
183
184
const experienceTypeGuards = {
185
  work: isWork,
186
  education: isEducation,
187
  community: isCommunity,
188
  award: isAward,
189
  personal: isPersonal,
190
};
191
192
function massageType(experienceType: Experience["type"]): EntityType {
193
  const mapping: { [key in Experience["type"]]: EntityType } = {
194
    experience_work: "work",
195
    experience_education: "education",
196
    experience_community: "community",
197
    experience_award: "award",
198
    experience_personal: "personal",
199
  };
200
  return mapping[experienceType];
201
}
202
203
function fetchExperienceByApplication<T extends EntityType>(
204
  state: EntityState,
205
  action: ExperienceAction,
206
  type: T,
207
): EntityState[T] {
208
  const subState = state[type];
209
  if (action.type !== FETCH_EXPERIENCE_BY_APPLICATION_SUCCEEDED) {
210
    return subState;
211
  }
212
  const typeFilter = experienceTypeGuards[type];
213
  const experiences = action.payload
214
    .map((response) => response.experience)
215
    .filter(typeFilter);
216
  return {
217
    ...subState,
218
    byId: {
219
      ...subState.byId,
220
      ...mapToObject(experiences, getId),
221
    },
222
    idsByApplication: {
223
      ...subState.idsByApplicant,
224
      [action.meta.applicationId]: experiences.map(getId),
225
    },
226
  };
227
}
228
function fetchExperienceByApplicant<T extends EntityType>(
229
  state: EntityState,
230
  action: ExperienceAction,
231
  type: T,
232
): EntityState[T] {
233
  const subState = state[type];
234
  if (action.type !== FETCH_EXPERIENCE_BY_APPLICANT_SUCCEEDED) {
235
    return subState;
236
  }
237
  const typeFilter = experienceTypeGuards[type];
238
  const experiences = action.payload
239
    .map((response) => response.experience)
240
    .filter(typeFilter);
241
  return {
242
    ...subState,
243
    byId: {
244
      ...subState.byId,
245
      ...mapToObject(experiences, getId),
246
    },
247
    idsByApplicant: {
248
      ...subState.idsByApplicant,
249
      [action.meta.applicantId]: experiences.map(getId),
250
    },
251
  };
252
}
253
254
function setExperience<T extends EntityType>(
255
  state: EntityState,
256
  action: ExperienceAction,
257
  type: T,
258
): EntityState[T] {
259
  const subState = state[type];
260
  if (
261
    (action.type !== CREATE_EXPERIENCE_SUCCEEDED &&
262
      action.type !== UPDATE_EXPERIENCE_SUCCEEDED) ||
263
    massageType(action.meta.type) !== type
264
  ) {
265
    return subState;
266
  }
267
  const { experience } = action.payload;
268
  const ownerId = experience.experienceable_id;
269
  const idsByApplicant =
270
    experience.experienceable_type === "applicant"
271
      ? {
272
          ...subState.idsByApplicant,
273
          [ownerId]: uniq([
274
            ...(subState.idsByApplicant[ownerId] ?? []),
275
            experience.id,
276
          ]),
277
        }
278
      : subState.idsByApplicant;
279
  const idsByApplication =
280
    experience.experienceable_type === "application"
281
      ? {
282
          ...subState.idsByApplication,
283
          [ownerId]: uniq([
284
            ...(subState.idsByApplication[ownerId] ?? []),
285
            experience.id,
286
          ]),
287
        }
288
      : subState.idsByApplication;
289
290
  return {
291
    ...subState,
292
    byId: {
293
      ...subState.byId,
294
      [experience.id]: experience,
295
    },
296
    idsByApplicant,
297
    idsByApplication,
298
  };
299
}
300
301
function deleteExperience<T extends EntityType>(
302
  state: EntityState,
303
  action: ExperienceAction,
304
  type: T,
305
): EntityState[T] {
306
  const subState = state[type];
307
  if (
308
    action.type !== DELETE_EXPERIENCE_SUCCEEDED ||
309
    massageType(action.meta.type) !== type
310
  ) {
311
    return subState;
312
  }
313
  const dropId = (ids: number[]): number[] =>
314
    ids.filter((id) => id !== action.meta.id);
315
  return {
316
    ...subState,
317
    byId: deleteProperty(subState.byId, action.meta.id),
318
    idsByApplicant: mapObjectValues(subState.idsByApplicant, dropId),
319
    idsByApplication: mapObjectValues(subState.idsByApplication, dropId),
320
  };
321
}
322
323
function setExperienceSkills(
324
  state: EntityState,
325
  experienceSkills: ExperienceSkill[],
326
): EntityState["experienceSkills"] {
327
  const newExpSkills = mapToObject(experienceSkills, getId);
328
  const workSkills = experienceSkills.filter(
329
    (expSkill) => expSkill.experience_type === "experience_work",
330
  );
331
  const educationSkills = experienceSkills.filter(
332
    (expSkill) => expSkill.experience_type === "experience_education",
333
  );
334
  const communitySkills = experienceSkills.filter(
335
    (expSkill) => expSkill.experience_type === "experience_community",
336
  );
337
  const awardSkills = experienceSkills.filter(
338
    (expSkill) => expSkill.experience_type === "experience_award",
339
  );
340
  const personalSkills = experienceSkills.filter(
341
    (expSkill) => expSkill.experience_type === "experience_personal",
342
  );
343
344
  interface ExpToSkillIds {
345
    [expId: number]: number[];
346
  }
347
  const reducer = (
348
    acc: ExpToSkillIds,
349
    expSkill: ExperienceSkill,
350
  ): ExpToSkillIds => {
351
    const prevIds = acc[expSkill.experience_id] ?? [];
352
    return {
353
      ...acc,
354
      [expSkill.experience_id]: uniq([expSkill.id, ...prevIds]),
355
    };
356
  };
357
  return {
358
    byId: { ...state.experienceSkills.byId, ...newExpSkills },
359
    idsByWork: workSkills.reduce(reducer, state.experienceSkills.idsByWork),
360
    idsByEducation: educationSkills.reduce(
361
      reducer,
362
      state.experienceSkills.idsByEducation,
363
    ),
364
    idsByCommunity: communitySkills.reduce(
365
      reducer,
366
      state.experienceSkills.idsByCommunity,
367
    ),
368
    idsByAward: awardSkills.reduce(reducer, state.experienceSkills.idsByAward),
369
    idsByPersonal: personalSkills.reduce(
370
      reducer,
371
      state.experienceSkills.idsByPersonal,
372
    ),
373
  };
374
}
375
376
const experienceSkillKeys = {
377
  experience_work: "idsByWork",
378
  experience_education: "idsByEducation",
379
  experience_community: "idsByCommunity",
380
  experience_award: "idsByAward",
381
  experience_personal: "idsByPersonal",
382
};
383
384
function deleteExpSkillsForExperience(
385
  state: EntityState,
386
  experienceId: number,
387
  experienceType: Experience["type"],
388
): EntityState["experienceSkills"] {
389
  const experienceKey = experienceSkillKeys[experienceType];
390
  const expSkillIds: number[] =
391
    state.experienceSkills[experienceKey][experienceId] ?? [];
392
  return {
393
    ...state.experienceSkills,
394
    [experienceKey]: deleteProperty(
395
      state.experienceSkills[experienceKey],
396
      experienceId,
397
    ),
398
    byId: expSkillIds.reduce(
399
      (byId, deleteId) => deleteProperty(byId, deleteId),
400
      state.experienceSkills.byId,
401
    ),
402
  };
403
}
404
405
function deleteExperienceSkill(
406
  state: EntityState,
407
  experienceSkillId: number,
408
  experienceId: number,
409
  experienceType: ExperienceSkill["experience_type"],
410
): EntityState["experienceSkills"] {
411
  const experienceKey = experienceSkillKeys[experienceType];
412
  return {
413
    ...state.experienceSkills,
414
    [experienceKey]: {
415
      ...state.experienceSkills[experienceKey],
416
      [experienceId]: state.experienceSkills[experienceKey][
417
        experienceId
418
      ].filter((id) => id !== experienceSkillId),
419
    },
420
    byId: deleteProperty(state.experienceSkills.byId, experienceSkillId),
421
  };
422
}
423
424
function batchDeleteExperienceSkills(
425
  state: EntityState,
426
  experienceSkills: ExperienceSkill[],
427
): EntityState {
428
  return experienceSkills.reduce((newState, expSkill) => {
429
    return {
430
      ...newState,
431
      experienceSkills: deleteExperienceSkill(
432
        newState,
433
        expSkill.id,
434
        expSkill.experience_id,
435
        expSkill.experience_type,
436
      ),
437
    };
438
  }, state);
439
}
440
441
export const entitiesReducer = (
442
  state = initEntities(),
443
  action: ExperienceAction,
444
): EntityState => {
445
  switch (action.type) {
446
    case FETCH_EXPERIENCE_BY_APPLICANT_SUCCEEDED:
447
      return {
448
        ...state,
449
        work: fetchExperienceByApplicant(state, action, "work"),
450
        education: fetchExperienceByApplicant(state, action, "education"),
451
        community: fetchExperienceByApplicant(state, action, "community"),
452
        award: fetchExperienceByApplicant(state, action, "award"),
453
        personal: fetchExperienceByApplicant(state, action, "personal"),
454
        experienceSkills: setExperienceSkills(
455
          state,
456
          flatten(action.payload.map((response) => response.experienceSkills)),
457
        ),
458
      };
459
    case FETCH_EXPERIENCE_BY_APPLICATION_SUCCEEDED:
460
      return {
461
        ...state,
462
        work: fetchExperienceByApplication(state, action, "work"),
463
        education: fetchExperienceByApplication(state, action, "education"),
464
        community: fetchExperienceByApplication(state, action, "community"),
465
        award: fetchExperienceByApplication(state, action, "award"),
466
        personal: fetchExperienceByApplication(state, action, "personal"),
467
        experienceSkills: setExperienceSkills(
468
          state,
469
          flatten(action.payload.map((response) => response.experienceSkills)),
470
        ),
471
      };
472
    case CREATE_EXPERIENCE_SUCCEEDED:
473
    case UPDATE_EXPERIENCE_SUCCEEDED:
474
      return {
475
        ...state,
476
        [massageType(action.meta.type)]: setExperience(
477
          state,
478
          action,
479
          massageType(action.meta.type),
480
        ),
481
      };
482
    case DELETE_EXPERIENCE_SUCCEEDED:
483
      return {
484
        ...state,
485
        [massageType(action.meta.type)]: deleteExperience(
486
          state,
487
          action,
488
          massageType(action.meta.type),
489
        ),
490
        experienceSkills: deleteExpSkillsForExperience(
491
          state,
492
          action.meta.id,
493
          action.meta.type,
494
        ),
495
      };
496
    case CREATE_EXPERIENCE_SKILL_SUCCEEDED:
497
    case UPDATE_EXPERIENCE_SKILL_SUCCEEDED:
498
      return {
499
        ...state,
500
        experienceSkills: setExperienceSkills(state, [action.payload]),
501
      };
502
    case DELETE_EXPERIENCE_SKILL_SUCCEEDED:
503
      return {
504
        ...state,
505
        experienceSkills: deleteExperienceSkill(
506
          state,
507
          action.meta.id,
508
          action.meta.experienceId,
509
          action.meta.experienceType,
510
        ),
511
      };
512
    case BATCH_CREATE_EXPERIENCE_SKILLS_SUCCEEDED:
513
    case BATCH_UPDATE_EXPERIENCE_SKILLS_SUCCEEDED:
514
      return {
515
        ...state,
516
        experienceSkills: setExperienceSkills(state, action.payload),
517
      };
518
    case BATCH_DELETE_EXPERIENCE_SKILLS_SUCCEEDED:
519
      return {
520
        ...batchDeleteExperienceSkills(state, action.meta),
521
      };
522
    default:
523
      return state;
524
  }
525
};
526
527
export const uiReducer = (
528
  state = initUi(),
529
  action: ExperienceAction,
530
): UiState => {
531
  switch (action.type) {
532
    case FETCH_EXPERIENCE_BY_APPLICANT_STARTED:
533
      return {
534
        ...state,
535
        updatingByApplicant: {
536
          ...state.updatingByApplicant,
537
          [action.meta.applicantId]: true,
538
        },
539
      };
540
    case FETCH_EXPERIENCE_BY_APPLICANT_SUCCEEDED:
541
    case FETCH_EXPERIENCE_BY_APPLICANT_FAILED:
542
      return {
543
        ...state,
544
        updatingByApplicant: {
545
          ...state.updatingByApplicant,
546
          [action.meta.applicantId]: false,
547
        },
548
      };
549
    case FETCH_EXPERIENCE_BY_APPLICATION_STARTED:
550
      return {
551
        ...state,
552
        updatingByApplication: {
553
          ...state.updatingByApplication,
554
          [action.meta.applicationId]: true,
555
        },
556
      };
557
    case FETCH_EXPERIENCE_BY_APPLICATION_SUCCEEDED:
558
    case FETCH_EXPERIENCE_BY_APPLICATION_FAILED:
559
      return {
560
        ...state,
561
        updatingByApplication: {
562
          ...state.updatingByApplication,
563
          [action.meta.applicationId]: false,
564
        },
565
      };
566
    case UPDATE_EXPERIENCE_STARTED:
567
    case DELETE_EXPERIENCE_STARTED:
568
      return {
569
        ...state,
570
        updatingByTypeAndId: {
571
          ...state.updatingByTypeAndId,
572
          [massageType(action.meta.type)]: {
573
            ...state.updatingByTypeAndId[massageType(action.meta.type)],
574
            [action.meta.id]: true,
575
          },
576
        },
577
      };
578
    case UPDATE_EXPERIENCE_SUCCEEDED:
579
    case DELETE_EXPERIENCE_SUCCEEDED:
580
    case UPDATE_EXPERIENCE_FAILED:
581
    case DELETE_EXPERIENCE_FAILED:
582
      return {
583
        ...state,
584
        updatingByTypeAndId: {
585
          ...state.updatingByTypeAndId,
586
          [massageType(action.meta.type)]: {
587
            ...state.updatingByTypeAndId[massageType(action.meta.type)],
588
            [action.meta.id]: false,
589
          },
590
        },
591
      };
592
    case UPDATE_EXPERIENCE_SKILL_STARTED:
593
    case DELETE_EXPERIENCE_SKILL_STARTED:
594
      return {
595
        ...state,
596
        updatingExperienceSkill: {
597
          ...state.updatingExperienceSkill,
598
          [action.meta.id]: true,
599
        },
600
      };
601
    case UPDATE_EXPERIENCE_SKILL_SUCCEEDED:
602
    case UPDATE_EXPERIENCE_SKILL_FAILED:
603
    case DELETE_EXPERIENCE_SKILL_SUCCEEDED:
604
    case DELETE_EXPERIENCE_SKILL_FAILED:
605
      return {
606
        ...state,
607
        updatingExperienceSkill: {
608
          ...state.updatingExperienceSkill,
609
          [action.meta.id]: false,
610
        },
611
      };
612
    default:
613
      return state;
614
  }
615
};
616
617
export const experienceReducer = combineReducers({
618
  entities: entitiesReducer,
619
  ui: uiReducer,
620
});
621
622
export default experienceReducer;
623